客户端能不等四次挥手就强制关闭 TCP 连接吗?

您所在的位置:网站首页 Java 文件写入时,强制关闭一个连接 客户端能不等四次挥手就强制关闭 TCP 连接吗?

客户端能不等四次挥手就强制关闭 TCP 连接吗?

2024-07-15 18:51| 来源: 网络整理| 查看: 265

在《Go 网络编程和 TCP 抓包实操》Conn.Close() 方法发起了关闭 TCP 连接的请求,这是一种默认的关闭连接方式。

默认关闭需要四次挥手的确认过程,这是一种”商量“的方式,而 TCP 为我们提供了另外一种”强制“的关闭模式。

如何强制性关闭?具体在 Go 代码中应当怎样实现?这就是本文探讨的内容。

默认关闭

相信每个程序员都知道 TCP 断开连接的四次挥手过程,这是面试八股文中的股中股。我们在 Go 代码中调用默认的 Conn.Close() 方法,它就是典型的四次挥手。

b79b7cc1b62bc761ac3c2b415b3c2c5d.png

img

以客户端主动关闭连接为例,当它调用 Close 函数后,就会向服务端发送 FIN 报文,如果服务器的本端 socket 接收缓存区里已经没有数据,那服务端的 read 将会得到一个 EOF 错误。

发起关闭方会经历 FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSE 的状态变化,这些状态需要得到被关闭方的反馈而更新。

强制关闭

默认的关闭方式,不管是客户端还是服务端主动发起关闭,都要经过对方的应答,才能最终实现真正的关闭连接。那能不能在发起关闭时,不关心对方是否同意,就结束掉连接呢?

答案是肯定的。TCP 协议为我们提供了一个 RST 的标志位,当连接的一方认为该连接异常时,可以通过发送 RST 包并立即关闭该连接,而不用等待被关闭方的 ACK 确认。

SetLinger() 方法

在 Go 中,我们可以通过 net.TCPConn.SetLinger() 方法来实现。

// SetLinger sets the behavior of Close on a connection which still // has data waiting to be sent or to be acknowledged. // // If sec  0, the data is sent in the background as with sec  0 时,操作系统会以与默认关闭方式运行。但是当超过定义的时间 sec 后,如果还没处理完缓存区的数据,在某些操作系统下,缓冲区中未完成的流量可能就会被丢弃。

而 sec == 0 时,操作系统会直接丢弃掉缓冲区里的流量数据,这就是强制性关闭。

示例代码与抓包分析

我们通过示例代码来学习 SetLinger() 的使用,并以此来分析强制关闭的区别。

服务端代码

以服务端为主动关闭连接方示例

package main import (  "log"  "net"  "time" ) func main() {  // Part 1: create a listener  l, err := net.Listen("tcp", ":8000")  if err != nil {   log.Fatalf("Error listener returned: %s", err)  }  defer l.Close()  for {   // Part 2: accept new connection   c, err := l.Accept()   if err != nil {    log.Fatalf("Error to accept new connection: %s", err)   }   // Part 3: create a goroutine that reads and write back data   go func() {    log.Printf("TCP session open")    defer c.Close()    for {     d := make([]byte, 100)     // Read from TCP buffer     _, err := c.Read(d)     if err != nil {      log.Printf("Error reading TCP session: %s", err)      break     }     log.Printf("reading data from client: %s\n", string(d))     // write back data to TCP client     _, err = c.Write(d)     if err != nil {      log.Printf("Error writing TCP session: %s", err)      break     }    }   }()   // Part 4: create a goroutine that closes TCP session after 10 seconds   go func() {    // SetLinger(0) to force close the connection    err := c.(*net.TCPConn).SetLinger(0)    if err != nil {     log.Printf("Error when setting linger: %s", err)    }    127.0.0.1:59394: use of closed network connection

服务器和客户端建立连接之后,从客户端读取到数据 Hi, gopher? 。在 10s 后,服务端强制关闭了 TCP 连接,阻塞在 c.Read 的服务端代码返回了错误: use of closed network connection。

运行客户端代码

$ go run main.go 2021/09/25 20:21:44 TCP session open 2021/09/25 20:21:44 reading data from server: Hi, gopher? 2021/09/25 20:21:54 Error reading TCP session: read tcp 127.0.0.1:59394->127.0.0.1:8000: read: connection reset by peer

客户端和服务器建立连接之后,发送数据给服务端,服务端返回相同的数据 Hi, gopher? 回来。在 10s 后,由于服务器强制关闭了 TCP 连接,因此阻塞在 c.Read 的客户端代码捕获到了错误:connection reset by peer。

tcpdump 的抓包结果

$ tcpdump -S -nn -vvv -i lo0 port 8000 tcpdump: listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes 20:21:44.682942 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64, bad cksum 0 (->3cb6)!)     127.0.0.1.59394 > 127.0.0.1.8000: Flags [S], cksum 0xfe34 (incorrect -> 0xfa62), seq 3783365585, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 725769370 ecr 0,sackOK,eol], length 0 20:21:44.683042 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64, bad cksum 0 (->3cb6)!)     127.0.0.1.8000 > 127.0.0.1.59394: Flags [S.], cksum 0xfe34 (incorrect -> 0x23d3), seq 1050611715, ack 3783365586, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 725769370 ecr 725769370,sackOK,eol], length 0 20:21:44.683050 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)     127.0.0.1.59394 > 127.0.0.1.8000: Flags [.], cksum 0xfe28 (incorrect -> 0x84dc), seq 3783365586, ack 1050611716, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 0 20:21:44.683055 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)     127.0.0.1.8000 > 127.0.0.1.59394: Flags [.], cksum 0xfe28 (incorrect -> 0x84dc), seq 1050611716, ack 3783365586, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 0 20:21:44.683302 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 63, bad cksum 0 (->3cb7)!)     127.0.0.1.59394 > 127.0.0.1.8000: Flags [P.], cksum 0xfe33 (incorrect -> 0x93f5), seq 3783365586:3783365597, ack 1050611716, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 11 20:21:44.683311 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)     127.0.0.1.8000 > 127.0.0.1.59394: Flags [.], cksum 0xfe28 (incorrect -> 0x84d1), seq 1050611716, ack 3783365597, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 0 20:21:44.683499 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 152, bad cksum 0 (->3c5e)!)     127.0.0.1.8000 > 127.0.0.1.59394: Flags [P.], cksum 0xfe8c (incorrect -> 0x9391), seq 1050611716:1050611816, ack 3783365597, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 100 20:21:44.683511 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)     127.0.0.1.59394 > 127.0.0.1.8000: Flags [.], cksum 0xfe28 (incorrect -> 0x846e), seq 3783365597, ack 1050611816, win 6378, options [nop,nop,TS val 725769370 ecr 725769370], length 0 20:21:54.688350 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40, bad cksum 0 (->3cce)!)     127.0.0.1.8000 > 127.0.0.1.59394: Flags [R.], cksum 0xfe1c (incorrect -> 0xcd39), seq 1050611816, ack 3783365597, win 6379, length 0

我们重点关注内容 Flags [],其中 [S] 代表 SYN 包,用于建立连接;[P] 代表 PSH 包,表示有数据传输;[R]代表 RST 包,用于重置连接;[.] 代表对应的 ACK 包。例如 [S.] 代表 SYN-ACK。

搞懂了这几个 Flags 的含义,那我们就可以分析出本次服务端强制关闭的 TCP 通信全过程。

d0eb345d331963d8bc14a6eed352cbd1.png img

我们和《Go 网络编程和 TCP 抓包实操》

159ab37276c886f4b63c20513ba99023.png img

可以看到,当通过设定 SetLinger(0) 之后,主动关闭方调用 Close() 时,系统内核会直接发送 RST 包给被动关闭方。这个过程并不需要被动关闭方的回复,就已关闭了连接。主动关闭方也就没有了默认关闭模式下 FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSE 的状态改变。

总结

本文我们介绍了 TCP 默认关闭与强制关闭两种方式(其实还有种折中的方式:SetLinger(sec > 0)),它们都源于 TCP 的协议设计。

在大多数的场景中,我们都应该选择使用默认关闭方式,因为这样才能确保数据的完整性(不会丢失 socket 缓冲区里的数据)。

当使用默认方式关闭时,每个连接都会经历一系列的连接状态转变,让其在操作系统上停留一段时间。尤其是服务器要主动关闭连接时(大多数应用场景,都应该是由客户端主动发起关闭操作),这会消耗服务器的资源。

如果短时间内有大量的或者恶意的连接涌入,我们或许需要采用强制关闭方式。因为使用强制关闭,能立即关闭这些连接,释放资源,保证服务器的可用与性能。

当然,我们还可以选择折中的方式,容忍一段时间的缓存区数据处理时间,再进行关闭操作。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3